﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace LucyGrid
{
	/// <summary>
	/// Manages workers heartbeat noatifications
	/// <remarks>Listen for worker heartbeats and notifies when a worker is dead</remarks>
	/// </summary>
	public class GridHeartbeat : IDisposable
	{
		/// <summary>
		/// Time to wait before selecting a node as dead
		/// </summary>
		public int GridNodeTimeout;
		/// <summary>
		/// Communication manager
		/// </summary>
		GridComm comm;
		/// <summary>
		/// Node timeout event definition
		/// </summary>
		/// <param name="nodeid"></param>
		public delegate void NodeTimeoutHandler(NodeInfo node);
		/// <summary>
		/// Event notification
		/// </summary>
		public event NodeTimeoutHandler OnNodeTimeout;

		/// <summary>
		/// Nodes cache
		/// </summary>
		Dictionary<string, NodeInfo> nodes;

		/// <summary>
		/// Endpoint to listen for
		/// </summary>
		private string endpoint;

		/// <summary>
		/// Subtasks control
		/// </summary>
		private bool heardbeatRunning;

		/// <summary>
		/// Builds a heardbeat manager with and enpoint to listen for and the minimum time to wait
		/// </summary>
		/// <param name="endpoint"></param>
		/// <param name="nodeTimeout"></param>
		public GridHeartbeat(string endpoint, int nodeTimeout)
		{
			heardbeatRunning = true;
			this.endpoint = endpoint;
			GridNodeTimeout = nodeTimeout;
			nodes = new Dictionary<string, NodeInfo>();
			// Listen to events
			comm = new GridComm().StartListener(null, OnReceiveEvents);
			// Run worker
			CheckTimeouts();
		}

		/// <summary>
		/// Rergister a new node working with the grid
		/// </summary>
		/// <param name="id"></param>
		/// <param name="address"></param>
		/// <returns></returns>
		public NodeInfo RegisterNode(string id, string address)
		{
			lock (nodes)
			{
				if (!nodes.ContainsKey(id))
				{
					var node = new NodeInfo()
					{
						Id = id,
						Address = address,
						Heartbeat = DateTime.Now
					};
					nodes.Add(id, node);
				}
				return nodes[id];
			}
		}

		/// <summary>
		/// Listen to communication events
		/// </summary>
		/// <param name="channel"></param>
		/// <param name="jobject"></param>
		private void OnReceiveEvents(GridCommEvents channel, JObject jobject)
		{
			switch (channel)
			{
				case GridCommEvents.heartbeat:
					// Check if a endpoint is set to filter
					if (jobject["GridEndpoint"].ToString() == this.endpoint)
					{
						var nodeid = jobject["NodeId"].ToString();
						if(nodes.ContainsKey(nodeid)){
							lock (nodes)
							{
								nodes[nodeid].Heartbeat = DateTime.Now;
							}
						}
					}
					break;
			}
		}

		/// <summary>
		/// Return a collection of nodes
		/// </summary>
		public IEnumerable<NodeInfo> Nodes
		{
			get { return nodes.Select(kv => kv.Value); }
		}

		/// <summary>
		/// Background process to check workers timeouts
		/// <remarks>Each worker marked as dead is removed and notified through the event OnNodeTimeout</remarks>
		/// </summary>
		private void CheckTimeouts()
		{
			Task.Run(() =>
			{
				Thread.CurrentThread.Name = string.Format("HEARTBEAT TIMEOUTS-{0}", Thread.CurrentThread.ManagedThreadId);
				IEnumerable<NodeInfo> res = null;
				while (heardbeatRunning)
				{
					if (OnNodeTimeout != null)
					{
						lock (nodes)
						{
							res = nodes.Where(n => (DateTime.Now - n.Value.Heartbeat).TotalMilliseconds > GridNodeTimeout).Select(kv => kv.Value).ToArray();
							foreach (var node in res)
							{
								nodes.Remove(node.Id);
							}
						}
						res.All(node =>
						{
							GridUtils.NotifyLog("HEARTBEAT,INFO", "Node timeout detected {0}", node);
							OnNodeTimeout(node);
							return true;
						});
					}
					Thread.Sleep(1000);
				}
			});
		}

		public void Dispose()
		{
			heardbeatRunning = false;
		}
	}
}
